package it.blueocean.acanto.security.authenticationManager;

import it.blueocean.acanto.entities.platform.DBUser;
import it.blueocean.acanto.security.authenticationManager.service.UserManagerService;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AbstractAuthenticationManager;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.SaltSource;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.security.authentication.encoding.PlaintextPasswordEncoder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

public class CustomAuthenticationManagerAndProvider extends AbstractAuthenticationManager implements
		AuthenticationManager, AuthenticationProvider, MessageSourceAware
{

	private final static Logger logger = LoggerFactory.getLogger(CustomAuthenticationManagerAndProvider.class);

	private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();

	private SaltSource saltSource;

	private boolean forcePrincipalAsString = false;

	private boolean includeDetailsObject = true;

	private UserManagerService userManagerService;

	private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	@Override
	public void setMessageSource(MessageSource messageSource)
	{
		this.messages = new MessageSourceAccessor(messageSource);
	}

	@Override
	public boolean supports(Class<? extends Object> authentication)
	{
		return authentication == UsernamePasswordAuthenticationToken.class;
	}

	@Override
	protected Authentication doAuthentication(Authentication authentication) throws AuthenticationException
	{
		logger.debug("Authentication attempt using" + CustomAuthenticationManagerAndProvider.class.getName());

		String principal = (String) authentication.getPrincipal();
		String credentials = (String) authentication.getCredentials();
		WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();

		if(StringUtils.isBlank(principal) || StringUtils.isBlank(credentials))
		{
			logger.warn("Invalid username/password : username [" + principal + "]");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials", "Invalid username/password"));
		}

		DBUser utenteBean = null;

		try
		{
			utenteBean = userManagerService.getUser(principal, credentials, details);
		}
		catch (Exception e)
		{
			logger.warn("Throws exception [" + e + "]. Username [" + principal + "]");
			throw new AuthenticationServiceException(e.getMessage());
		}

		if(utenteBean != null)
		{
			additionalAuthenticationChecks(utenteBean, (UsernamePasswordAuthenticationToken) authentication);
			Object principalToReturn = utenteBean;
			if(forcePrincipalAsString)
			{
				principalToReturn = utenteBean.getUsername();
			}
			
			logger.info("User: [" + principalToReturn + "] : +++++ login OK +++++");
			
			return createSuccessAuthentication(principalToReturn, authentication, utenteBean);
		}

		logger.warn("Bad credentials : username [" + principal + "]");

		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
	}

	/**
	 * Creates a successful {@link Authentication} object.
	 * <p>
	 * Protected so subclasses can override.
	 * </p>
	 * <p>
	 * Subclasses will usually store the original credentials the user supplied
	 * (not salted or encoded passwords) in the returned
	 * <code>Authentication</code> object.
	 * </p>
	 * 
	 * @param principal
	 *            that should be the principal in the returned object (defined
	 *            by the {@link #isForcePrincipalAsString()} method)
	 * @param authentication
	 *            that was presented to the provider for validation
	 * @param user
	 *            that was loaded by the implementation
	 * 
	 * @return the successful authentication token
	 */
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user)
	{
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
				authentication.getCredentials(), user.getAuthorities());
		result.setDetails(authentication.getDetails());

		return result;
	}

	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException
	{
		Object salt = null;

		if(this.saltSource != null)
		{
			salt = this.saltSource.getSalt(userDetails);
		}

		if(authentication.getCredentials() == null)
		{
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"),
					includeDetailsObject ? userDetails : null);
		}

		String presentedPassword = authentication.getCredentials().toString();

		if(!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt))
		{
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"),
					includeDetailsObject ? userDetails : null);
		}
	}

	public void setPasswordEncoder(PasswordEncoder passwordEncoder)
	{
		this.passwordEncoder = passwordEncoder;
	}

	public void setSaltSource(SaltSource saltSource)
	{
		this.saltSource = saltSource;
	}

	public void setForcePrincipalAsString(boolean forcePrincipalAsString)
	{
		this.forcePrincipalAsString = forcePrincipalAsString;
	}

	public void setIncludeDetailsObject(boolean includeDetailsObject)
	{
		this.includeDetailsObject = includeDetailsObject;
	}

	public void setUserManagerService(UserManagerService userManagerService)
	{
		this.userManagerService = userManagerService;
	}
}
